昨天我把一次共享資源預約分成 Understand、Propose、Authorize、Commit 與 Recover,也實際測了 typed contract、核准與 RBAC 三條邊界。
但這些邊界如果只散落在幾個 function 裡,後面很快就會出現新問題:誰可以改狀態?誰可以呼叫有 side effect 的 tool?自然語言內容和授權資訊應該在哪裡分開?
所以今天我不是先畫一套理想架構,而是回頭盤點現在這套 Reference System 裡,每個責任實際落在哪裡。
現在的程式沒有 Web UI,也沒有外部 LLM。這反而讓控制面更容易看清楚:
| 責任區 | 現在的實作 | 不應該做的事 |
|---|---|---|
| Conversation | raw_request 與未來的 LLM adapter |
不能當權限或 workflow state |
| Typed Intent | BookingIntent.from_mapping() |
不根據自然語言自行補權限 |
| Orchestrator | WorkflowService.start_booking_workflow() |
不跳過 state transition 直接 commit |
| Tool | BookingTools |
不暴露 raw SQL 或 database connection |
| Policy | evaluate_policy() 與 _policy_for() |
不從 prompt 取得放行權 |
| State | SQLite workflow_runs 與 state_transitions |
不用聊天紀錄推測當前狀態 |
| Audit / Trace | audit_log、trace_id |
不只保留最後一句回覆 |
這七個責任並不等於七個微服務,目前可以留在同一個程式和 SQLite 裡,只要 API 與資料擁有權的邊界清楚。我不想為了看起來像「企業架構」,在還沒有負載和團隊邊界前就先拆成一堆服務。
Reference System 對自然語言有一個很刻意的設計:raw_request 只是 untrusted context。真正進入流程的是已經通過 schema validation 的 BookingIntent。
def start_booking_workflow(
self,
actor_id: str,
intent_payload: Mapping[str, Any],
*,
idempotency_key: str,
raw_request: str | None = None,
) -> WorkflowSnapshot:
...
我很喜歡這個 function signature 呈現出來的邊界:
actor_id 是身份,不從句子裡猜。intent_payload 是將要被驗證的結構化輸入。idempotency_key 是重試與副作用的控制資訊。raw_request 可以幫助理解與審計,卻不具有授權力。未來接上 LLM 時,模型會在 Conversation 和 Typed Intent 之間,不是在 Conversation 和 Database 之間。
WorkflowService 會沿著狀態機執行、呼叫資源查詢、要求確認,並在條件滿足後進入 commit,但不應該把「流程現在走到哪裡」和「這個人能不能做」混成同一個 if statement。
現在的 policy decision 是一個明確物件:
@dataclass(frozen=True)
class PolicyDecision:
allowed: bool
requires_approval: bool
reason: str
這讓 Orchestrator 可以依 decision 推進或停止,同時把 reason 留在 audit。未來 policy 就算從 Python 移到其他引擎,Orchestrator 需要的 contract 也不必跟著改寫。
BookingTools 只開放五個 typed methods:
search_resources
get_availability
create_booking
cancel_booking
get_booking
沒有 execute_sql,也沒有一個萬用的 call_backend(action, payload)。這個限制不是為了少寫功能,而是讓每個 side effect 都能有自己的 schema、授權、idempotency 與 audit 規則。
從 Day 03 的測試可以看到,即使直接呼叫 create_booking,high-impact request 仍然會被 policy 擋下。也就是說,控制不只在 Orchestrator 的正常路徑上,Tool commit 邊界還會再檢查一次。
我把這兩個概念分開:
workflow_runs.state 回答現在在哪裡。state_transitions 回答經過哪些轉移才到這裡。audit_log 回答誰在什麼時候做了哪個動作,結果為何。trace_id 把同一次流程跨元件的事件串起來。如果只有最後的 SUCCEEDED,我不知道中間是否真的經過確認和核准。如果只有 audit events,卻沒有明確 state,服務重啟後又不知道該從哪裡繼續。這兩者不能互相取代。
我重跑了有確認與核准的路徑:
python3 -m unittest \
tests.test_agentic_workflow.AgenticWorkflowIntegrationTests.test_confirmation_approval_state_audit_and_trace_are_persisted \
-v
結果:
test_confirmation_approval_state_audit_and_trace_are_persisted ... ok
Ran 1 test
OK
這個 test 同時穿過了 typed intent、orchestrator、policy、state、tool 與 audit。我最在意的不是最後 OK,而是還驗證這條狀態鏈:
NEW
-> UNDERSTOOD
-> SEARCHED
-> AWAIT_CONFIRMATION
-> APPROVED
-> EXECUTING
-> SUCCEEDED
也重新建立 WorkflowService 後讀回 AWAIT_CONFIRMATION,證明暫停中的狀態不是只留在 Python object 或對話文字裡。
這次驗證足以說明責任邊界有對應的程式和資料,但不代表:
是一套可以持續加入失敗、重試、衝突與攻擊測試的 reference architecture,不是架構完成宣言。
盤點完實作後,我對 Agentic Workflow 的架構原則比較明確了:
Conversation 承載使用者語意,Orchestrator 推進流程,Policy 決定能不能做,Tool 負責受控的 side effect,State 保存事實,Audit 證明如何發生。
下一篇我會把焦點收斂到 State:哪些狀態是必要的、哪些 transition 應該被禁止,以及為什麼對話紀錄無法代替狀態機。
下一篇:Day 05|先定 State:企業流程不是一串 Prompt
本篇只描述 repository 中已實作並可測試的邊界,未接上真實企業系統或外部模型。